/**
 * Copyright 2014 ABSir's Studio
 * 
 * All right reserved
 *
 * Create on 2014-2-17 下午4:06:43
 */
package com.absir.server.socket;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Map;

import com.absir.bean.basis.Configure;
import com.absir.bean.inject.value.Inject;
import com.absir.bean.inject.value.InjectType;
import com.absir.bean.inject.value.Value;
import com.absir.context.core.ContextUtils;
import com.absir.core.kernel.KernelByte;
import com.absir.core.kernel.KernelDyna;
import com.absir.server.exception.ServerStatus;
import com.absir.server.in.InMethod;
import com.absir.server.in.InModel;
import com.absir.server.in.Input;
import com.absir.server.socket.resolver.SocketChannelResolver;

/**
 * @author absir
 * 
 */
@Configure
public abstract class InputSocket extends Input {

	/** NONE_RESPONSE */
	public static final String NONE_RESPONSE = "";

	/** NONE_RESPONSE_BYTES */
	public static final byte[] NONE_RESPONSE_BYTES = NONE_RESPONSE.getBytes();

	/** socketChannel */
	private SocketChannel socketChannel;

	/** uri */
	private String uri;

	/** status */
	private int status = ServerStatus.ON_SUCCESS.getCode();

	/** flag */
	private byte flag;

	/** input */
	private int callbackIndex;

	/** input */
	private String input;

	/** inputStream */
	private byte[] inputBuffer;

	/** inputPos */
	private int inputPos;

	/** inputCount */
	private int inputCount;

	/**
	 * @param model
	 * @param inputSocketAtt
	 * @param socketChannel
	 */
	public InputSocket(InModel model, InputSocketAtt inputSocketAtt, SocketChannel socketChannel) {
		super(model);
		this.socketChannel = socketChannel;
		setId(inputSocketAtt.getId());
		uri = inputSocketAtt.getUrl();
		flag = inputSocketAtt.getFlag();
		callbackIndex = inputSocketAtt.getCallbackIndex();
		inputBuffer = inputSocketAtt.getBuffer();
		if (inputBuffer != null) {
			inputCount = inputSocketAtt.getPostDataLength();
			inputPos = inputBuffer.length - inputCount;
		}
	}

	/**
	 * @author absir
	 * 
	 */
	public static class InputSocketAtt {

		/** DEBUG_FLAG */
		public static final byte DEBUG_FLAG = (byte) (0x01 << 7);

		/** CALLBACK_FLAG */
		public static final byte CALLBACK_FLAG = 0x01 << 6;

		/** POST_FLAG */
		public static final byte POST_FLAG = 0x01 << 5;

		/** ERROR_FLAG */
		public static final byte ERROR_FLAG = 0x01 << 4;

		/** RESPONSE_FLAG */
		public static final byte RESPONSE_FLAG = 0x01 << 3;

		/** id */
		protected Serializable id;

		/** buffer */
		protected byte[] buffer;

		/** flag */
		protected byte flag;

		/** callbackIndex */
		protected int callbackIndex;

		/** url */
		protected String url;

		/** postDataLength */
		protected int postDataLength;

		/**
		 * 空初始化
		 */
		protected InputSocketAtt() {
		}

		/**
		 * @param id
		 * @param buffer
		 */
		public InputSocketAtt(Serializable id, byte[] buffer) {
			this(id, buffer, 0);
		}

		/**
		 * @param id
		 * @param buffer
		 * @param off
		 */
		public InputSocketAtt(Serializable id, byte[] buffer, int off) {
			this.id = id;
			this.buffer = buffer;
			this.flag = buffer[off];
			int headerlength = off + 1;
			if ((flag & CALLBACK_FLAG) != 0) {
				callbackIndex = KernelByte.getLength(buffer, headerlength);
				headerlength += 4;
			}

			if ((flag & POST_FLAG) != 0) {
				postDataLength = KernelByte.getLength(buffer, headerlength);
				headerlength += 4;
			}

			url = new String(buffer, headerlength, buffer.length - headerlength - postDataLength, ContextUtils.getCharset());
		}

		/**
		 * @return the id
		 */
		public Serializable getId() {
			return id;
		}

		/**
		 * @return the url
		 */
		public String getUrl() {
			return url;
		}

		/**
		 * @return the flag
		 */
		public byte getFlag() {
			return flag;
		}

		/**
		 * @return
		 */
		public InMethod getMethod() {
			return (flag & POST_FLAG) == 0 ? InMethod.GET : InMethod.POST;
		}

		/**
		 * @return the callbackIndex
		 */
		public int getCallbackIndex() {
			return callbackIndex;
		}

		/**
		 * @return the buffer
		 */
		public byte[] getBuffer() {
			return buffer;
		}

		/**
		 * @return the postDataLength
		 */
		public int getPostDataLength() {
			return postDataLength;
		}
	}

	/**
	 * @return the socketChannel
	 */
	public SocketChannel getSocketChannel() {
		return socketChannel;
	}

	@Override
	public Object getAttribute(String name) {
		return getModel().get(name);
	}

	@Override
	public void setAttribute(String name, Object obj) {
		getModel().put(name, obj);
	}

	@Override
	public String getUri() {
		return uri;
	}

	@Override
	public Map<String, Object> getParamMap() {
		return getModel();
	}

	@Override
	public String getParam(String name) {
		return KernelDyna.to(getModel().get(name), String.class);
	}

	@Override
	public InMethod getMethod() {
		return input == null ? InMethod.GET : InMethod.POST;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.absir.server.in.Input#setStatus(int)
	 */
	@Override
	public void setStatus(int status) {
		this.status = status;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.absir.server.in.Input#paramDebug()
	 */
	@Override
	public boolean paramDebug() {
		return (flag & InputSocketAtt.DEBUG_FLAG) != 0;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.absir.server.in.Input#getAddress()
	 */
	@Override
	public String getAddress() {
		return socketChannel.socket().getLocalAddress().getHostAddress();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.absir.server.in.Input#getParams(java.lang.String)
	 */
	@Override
	public String[] getParams(String name) {
		return null;
	}

	@Override
	public InputStream getInputStream() throws IOException {
		return inputBuffer == null ? null : new ByteArrayInputStream(inputBuffer, inputPos, inputCount);
	}

	@Override
	public String getInput() {
		if (input == null && inputBuffer != null) {
			input = new String(inputBuffer, inputPos, inputCount, ContextUtils.getCharset());
			inputBuffer = null;
		}

		return input;
	}

	@Override
	public void setContentTypeCharset(String contentTypeCharset) {
	}

	@Override
	public void setCharacterEncoding(String charset) {
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.absir.server.in.Input#getOutputStream()
	 */
	@Override
	public OutputStream getOutputStream() throws IOException {
		return null;
	}

	/**
	 * @return
	 */
	public abstract SocketChannelResolver getSocketChannelResolver();

	/**
	 * @return
	 */
	public Socket getSocket() {
		return socketChannel.socket();
	}

	/**
	 * @param flag
	 */
	protected byte writeFlag(byte flag) {
		if (status != ServerStatus.ON_SUCCESS.getCode()) {
			flag |= InputSocketAtt.ERROR_FLAG;
		}

		return flag;
	}

	@Override
	public void write(byte[] b, int off, int len) throws IOException {
		if (socketChannel == null) {
			writeByteBuffer(getSocketChannelResolver(), getSocket(), writeFlag(flag), callbackIndex, b);

		} else {
			writeByteBuffer(getSocketChannelResolver(), socketChannel, writeFlag(flag), callbackIndex, b);
		}
	}

	/**
	 * @param socketChannel
	 */
	public static void close(SocketChannel socketChannel) {
		try {
			socketChannel.close();

		} catch (Throwable e) {
		}
	}

	/**
	 * @param socket
	 */
	public static void close(Socket socket) {
		try {
			socket.close();

		} catch (Throwable e) {
		}
	}

	/**
	 * @param socketChannel
	 * @param buffer
	 * @return
	 */
	public static boolean writeChannelBuffer(SocketChannel socketChannel, byte[] buffer) {
		try {
			if (socketChannel.isBlocking()) {
				socketChannel.socket().getOutputStream().write(buffer);

			} else {
				ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
				synchronized (socketChannel) {
					writeTimeout(socketChannel, byteBuffer);
				}
			}

			return true;

		} catch (Throwable e) {
			close(socketChannel);
			return false;
		}
	}

	/**
	 * @param channelResolver
	 * @param socketChannel
	 * @param bytes
	 * @return
	 */
	public static boolean writeByteBuffer(SocketChannelResolver channelResolver, SocketChannel socketChannel, byte[] bytes) {
		byte[] buffer = channelResolver.writeByteBuffer(socketChannel, 0, bytes);
		return writeChannelBuffer(socketChannel, buffer);
	}

	/**
	 * @param channelResolver
	 * @param socketChannel
	 * @param callbackIndex
	 * @param bytes
	 * @return
	 */
	public static boolean writeByteBuffer(SocketChannelResolver channelResolver, SocketChannel socketChannel, int callbackIndex,
			byte[] bytes) {
		return writeByteBuffer(channelResolver, socketChannel, (byte) 0, callbackIndex, bytes);
	}

	/**
	 * @param channelResolver
	 * @param socketChannel
	 * @param flag
	 * @param callbackIndex
	 * @param bytes
	 * @return
	 */
	public static boolean writeByteBuffer(SocketChannelResolver channelResolver, SocketChannel socketChannel, byte flag,
			int callbackIndex, byte[] bytes) {
		int headerLength = callbackIndex == 0 ? 1 : 5;
		byte[] buffer = channelResolver.writeByteBuffer(null, headerLength, bytes);
		headerLength = buffer.length - bytes.length - headerLength;
		if (callbackIndex != 0) {
			flag |= InputSocketAtt.CALLBACK_FLAG;
			KernelByte.setLength(buffer, headerLength + 1, callbackIndex);
		}

		buffer[headerLength] = flag;
		try {
			ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
			synchronized (socketChannel) {
				writeTimeout(socketChannel, byteBuffer);
			}

			return true;

		} catch (Throwable e) {
			close(socketChannel);
			return false;
		}
	}

	/**
	 * @param channelResolver
	 * @param socket
	 * @param flag
	 * @param callbackIndex
	 * @param bytes
	 * @return
	 */
	public static boolean writeByteBuffer(SocketChannelResolver channelResolver, Socket socket, byte flag, int callbackIndex,
			byte[] bytes) {
		SocketChannel socketChannel = socket.getChannel();
		if (socketChannel == null || socketChannel.isBlocking()) {
			int headerLength = callbackIndex == 0 ? 1 : 5;
			byte[] buffer = channelResolver.writeByteBuffer(null, headerLength, bytes);
			headerLength = buffer.length - bytes.length - headerLength;
			if (callbackIndex != 0) {
				flag |= InputSocketAtt.CALLBACK_FLAG;
				KernelByte.setLength(buffer, headerLength + 1, callbackIndex);
			}

			buffer[headerLength] = flag;
			try {
				socket.getOutputStream().write(buffer);
				return true;

			} catch (Throwable e) {
				close(socket);
				return false;
			}

		} else {
			return writeByteBuffer(channelResolver, socketChannel, flag, callbackIndex, bytes);
		}
	}

	/**
	 * @param channelResolver
	 * @param socketChannel
	 * @param success
	 * @param callbackIndex
	 * @param bytes
	 * @return
	 */
	public static boolean writeByteBufferSuccess(SocketChannelResolver channelResolver, SocketChannel socketChannel,
			boolean success, int callbackIndex, byte[] bytes) {
		return writeByteBuffer(channelResolver, socketChannel, success == true ? 0 : InputSocketAtt.ERROR_FLAG, callbackIndex,
				bytes);
	}

	/** writeTimeout */
	private static long writeTimeout = 10000;

	/**
	 * @param writeTimeout
	 *            the writeTimeout to set
	 */
	@Inject(type = InjectType.Selectable)
	public static void setWriteTimeout(@Value("server.socket.write.timeout") long timeout) {
		if (timeout >= 1000) {
			writeTimeout = timeout;
		}
	}

	/**
	 * @param size
	 */
	@Inject(type = InjectType.Selectable)
	public static void setSelectFactory(@Value("server.socket.selector.pool.size") int size) {
		if (size > 0) {
			SelectorFactory.setMaxSelectors(size);
		}
	}

	/**
	 * @param socketChannel
	 * @param byteBuffer
	 * @return
	 * @throws IOException
	 */
	public static void writeTimeout(SocketChannel socketChannel, ByteBuffer byteBuffer) throws IOException {
		writeTimeout(socketChannel, byteBuffer, writeTimeout);
	}

	/**
	 * @param socketChannel
	 * @param byteBuffer
	 * @param writeTimeout
	 * @return
	 * @throws IOException
	 */
	public static void writeTimeout(SocketChannel socketChannel, ByteBuffer byteBuffer, long writeTimeout) throws IOException {
		int attempts = 0;
		SelectionKey key = null;
		Selector writeSelector = null;
		try {
			while (byteBuffer.hasRemaining()) {
				int len = socketChannel.write(byteBuffer);
				attempts++;
				if (len <= 0) {
					if (len < 0) {
						throw new EOFException();

					} else {
						if (writeSelector == null) {
							writeSelector = SelectorFactory.getSelector();
							if (writeSelector == null) {
								// continue using the main one
								continue;
							}

							key = socketChannel.register(writeSelector, SelectionKey.OP_WRITE);
						}

						if (writeSelector.select(writeTimeout) == 0) {
							if (attempts > 2)
								throw new IOException("client disconnected");

						} else {
							attempts--;
						}
					}

				} else {
					attempts = 0;
				}
			}

		} finally {
			if (key != null) {
				key.cancel();
				key = null;
			}

			if (writeSelector != null) {
				writeSelector.selectNow();
				SelectorFactory.returnSelector(writeSelector);
			}
		}
	}
}
